﻿using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Collections.Generic;
using System.Windows.Input;

namespace OnlinePlots.Diagrams
{
    public abstract class DiagramControl : ItemsControl
    {
        internal const string ElementRootName = "PlotsRootLayer";

        protected Canvas root;
        protected DiagramElement diagramElementsContainer;

        protected double logicalFrameXMin = 0;
        protected double logicalFrameXMax = 10;
        protected double logicalFrameYMin = 0;
        protected double logicalFrameYMax = 10;

        private double framesHorizontalRatio = 1;
        private double framesVerticalRatio = 1;

        public event EventHandler DiagramShapesPrepared;
        public event EventHandler LogicalFrameZoomed;

        public DiagramControl()
            : base()
        {
            DefaultStyleKey = typeof(DiagramControl);

            diagramElementsContainer = new DiagramElement(this);
            ApplyTemplate();
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (root.ActualHeight != finalSize.Height || root.ActualWidth != finalSize.Width)
            {
                Size arrangedSize = base.ArrangeOverride(finalSize);

                RecalculateRatios();
                Redraw();

                return arrangedSize;
            }
            else
            {
                return base.ArrangeOverride(finalSize);
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            root = GetTemplateChild(ElementRootName) as Canvas;
        }

        public static DependencyProperty RightMarginProperty = DependencyProperty.Register(
                "RightMargin",
                typeof(int),
                typeof(DiagramControl),
                new PropertyMetadata(10)
            );

        /// <summary>
        /// Gets or sets the right margin in pixels
        /// </summary>
        public int RightMargin
        {
            get { return (int)GetValue(RightMarginProperty); }
            set { SetValue(RightMarginProperty, value); }
        }

        public static DependencyProperty LeftMarginProperty = DependencyProperty.Register(
                "LeftMargin",
                typeof(int),
                typeof(DiagramControl),
                new PropertyMetadata(10)
            );

        /// <summary>
        /// Gets or sets the left margin in pixels
        /// </summary>
        public int LeftMargin
        {
            get { return (int)GetValue(LeftMarginProperty); }
            set { SetValue(LeftMarginProperty, value); }
        }

        public static DependencyProperty TopMarginProperty = DependencyProperty.Register(
                "TopMargin",
                typeof(int),
                typeof(DiagramControl),
                new PropertyMetadata(10)
            );

        /// <summary>
        /// Gets or sets the top margin in pixels
        /// </summary>
        public int TopMargin
        {
            get { return (int)GetValue(TopMarginProperty); }
            set { SetValue(TopMarginProperty, value); }
        }

        public static DependencyProperty BottomMarginProperty = DependencyProperty.Register(
                "BottomMargin",
                typeof(int),
                typeof(DiagramControl),
                new PropertyMetadata(10)
            );

        /// <summary>
        /// Gets or sets the bottom margin in pixels
        /// </summary>
        public int BottomMargin
        {
            get { return (int)GetValue(BottomMarginProperty); }
            set { SetValue(BottomMarginProperty, value); }
        }

        /// <summary>
        /// Gets or sets the minimum x-axis value of logical frame. Logical frame is a rectangle
        /// that corresponds to the currently visible plot area
        /// </summary>
        public double LogicalFrameXMin
        {
            get { return logicalFrameXMin; }
            set
            {
                if (value < logicalFrameXMax)
                {
                    logicalFrameXMin = value;
                    RecalculateRatios();
                }
            }
        }

        /// <summary>
        /// Gets or sets the maximum x-axis value of logical frame. Logical frame is a rectangle
        /// that corresponds to the currently visible plot area
        /// </summary>
        public double LogicalFrameXMax
        {
            get { return logicalFrameXMax; }
            set
            {
                if (value > logicalFrameXMin)
                {
                    logicalFrameXMax = value;
                    RecalculateRatios();
                }
            }
        }

        /// <summary>
        /// Gets or sets the minimum y-axis value of logical frame. Logical frame is a rectangle
        /// that corresponds to the currently visible plot area
        /// </summary>
        public double LogicalFrameYMin
        {
            get { return logicalFrameYMin; }
            set
            {
                if (value < logicalFrameYMax)
                {
                    logicalFrameYMin = value;
                    RecalculateRatios();
                }
            }
        }

        /// <summary>
        /// Gets or sets the maximum y-axis value of logical frame. Logical frame is a rectangle
        /// that corresponds to the currently visible plot area
        /// </summary>
        public double LogicalFrameYMax
        {
            get { return logicalFrameYMax; }
            set
            {
                if (value > logicalFrameYMin)
                {
                    logicalFrameYMax = value;
                    RecalculateRatios();
                }
            }
        }

        /// <summary>
        /// Gets current width of the logical frame. Logical frame is a rectangle
        /// that corresponds to the currently visible plot area
        /// </summary>
        public double LogicalFrameWidth
        {
            get { return logicalFrameXMax - logicalFrameXMin; }
        }

        /// <summary>
        /// Gets current height of the logical frame. Logical frame is a rectangle
        /// that corresponds to the currently visible plot area
        /// </summary>
        public double LogicalFrameHeight
        {
            get { return logicalFrameYMax - logicalFrameYMin; }
        }

        /// <summary>
        /// Gets the list of custom shapes drawn on the plot area
        /// </summary>
        public IList<UIElement> CustomShapes
        {
            get { return diagramElementsContainer.Shapes; }
        }

        private void RecalculateRatios()
        {
            framesHorizontalRatio = Math.Max(1, (double)(root.ActualWidth - (RightMargin + LeftMargin))) / LogicalFrameWidth;
            framesVerticalRatio = Math.Max(1, (double)(root.ActualHeight - (TopMargin + BottomMargin))) / LogicalFrameHeight;
        }

        protected void OnDiagramShapesPrepared()
        {
            if (DiagramShapesPrepared != null)
            {
                DiagramShapesPrepared(this, new EventArgs());
            }
        }

        protected virtual void ClearDiagramShapes()
        {
            diagramElementsContainer.ClearAllShapes();
        }

        /*protected*/ void DrawDiagramShapes()
        {
            Dictionary<int, List<UIElement>> shapesSortedByZOrder = new Dictionary<int, List<UIElement>>();
            diagramElementsContainer.GetAllShapes().ForEach(shape => root.Children.Add(shape));
        }

        protected virtual void PrepareDiagramShapes()
        {

        }

        /// <summary>
        /// Redraws the diagram area
        /// </summary>
        public virtual void Redraw()
        {
            ClearDiagramShapes();
            if (root.ActualWidth > 0 && root.ActualWidth > 0)
            {
                root.Children.Clear();
                PrepareDiagramShapes();
                OnDiagramShapesPrepared();
                DrawDiagramShapes();
            }
        }

        /// <summary>
        /// Transforms the coordinates in the diagram logical space to the point
        /// in the physical frame of the diagram
        /// </summary>
        /// <param name="logicalX">X-coordinate of the point in the logical space</param>
        /// <param name="logicalY">Y-coordinate of the point in the logical space</param>
        /// <returns>Point in the physical frame of the diagram</returns>
        public Point FromLogicalToPhysical(double logicalX, double logicalY)
        {
            double x = LeftMargin + framesHorizontalRatio * (logicalX - logicalFrameXMin);
            double y = root.ActualHeight - framesVerticalRatio * (logicalY - logicalFrameYMin) - BottomMargin;
            return new Point(x, y);
        }

        /// <summary>
        /// Transforms the coordinates in diagram physical frame to the point
        /// in the logical space of the diagram
        /// </summary>
        /// <param name="logicalX">X-coordinate of the point in the logical space</param>
        /// <param name="logicalY">Y-coordinate of the point in the logical space</param>
        /// <returns>Point in the logical space of the diagram</returns>
        public Point FromPhysicalToLogical(double physicalX, double physicalY)
        {
            double x = (physicalX - LeftMargin) / framesHorizontalRatio + logicalFrameXMin;
            double y = logicalFrameYMin + (root.ActualHeight - physicalY - BottomMargin) / framesVerticalRatio;
            return new Point(x, y);
        }

        /// <summary>
        /// Moves the logical frame to the certain distance along x any y axes
        /// </summary>
        /// <param name="dx">Distance to move along x-axis</param>
        /// <param name="dy">Distance to move along y-axis</param>
        public void MoveLogicalFrame(double dx, double dy)
        {
            logicalFrameXMin += dx;
            logicalFrameXMax += dx;
            logicalFrameYMin += dy;
            logicalFrameYMax += dy;
            RecalculateRatios();
        }

        /// <summary>
        /// Moves the logical frame so that it has the center in the specified point
        /// </summary>
        /// <param name="x">X-coordinate of the desired center point</param>
        /// <param name="y">Y-coordinate of the desired center point</param>
        public void CenterLogicalFrameAtPoint(double x, double y)
        {
            double dx = x - (logicalFrameXMin + LogicalFrameWidth / 2);
            double dy = y - (logicalFrameYMin + LogicalFrameHeight / 2);
            MoveLogicalFrame(dx, dy);
        }

        /// <summary>
        /// Zooms (scales) the logical due to the specified zoom factor
        /// </summary>
        /// <param name="zoomFactor">Zoom factor value</param>
        public void ZoomLogicalFrame(double zoomFactor)
        {
            double width = LogicalFrameWidth;
            double height = LogicalFrameHeight;
            if ((logicalFrameXMax - logicalFrameXMin > 0.0001 || zoomFactor > 1) && 
                (logicalFrameXMax - logicalFrameXMin < 100000 || zoomFactor < 1))
            {
                logicalFrameXMin += width * (1 - zoomFactor) / 2;
                logicalFrameXMax -= width * (1 - zoomFactor) / 2;
            }

            if ((logicalFrameYMax - logicalFrameYMin > 0.0001 || zoomFactor > 1) && 
                (logicalFrameYMax - logicalFrameYMin < 100000 || zoomFactor < 1))
            {
                logicalFrameYMin += height * (1 - zoomFactor) / 2;
                logicalFrameYMax -= height * (1 - zoomFactor) / 2;
            }

            RecalculateRatios();
            if (LogicalFrameZoomed != null) LogicalFrameZoomed(this, EventArgs.Empty);
        }
    }
}